package org.jseries.network;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;

import org.jseries.network.utils.ScanNet;
import org.jseries.network.utils.ScanNet.InetRange;

public class NetworkWatcherApp {

	public static final SimpleDateFormat FORMAT_DATE_TIME = new SimpleDateFormat("E, M/d/y h:mm:ss a");
	
	private ActiveDeviceModel model = new ActiveDeviceModel();
	Timer timer = new Timer("Scan Netowrk", true);
	ScanNetworkTask scanNetworkTask = new ScanNetworkTask();
	
	//external variables. 
	String networkRange = "192.168.1.255/24"; //TODO make an external property 
	
	private JFrame frmNetworkWatcher;
	private JTable table;

	/**
	 * Launch the application.
	 */
	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				try {
					NetworkWatcherApp window = new NetworkWatcherApp();
					window.frmNetworkWatcher.setVisible(true);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}

	/**
	 * Create the application.
	 */
	public NetworkWatcherApp() {
		initialize();
	}

	/**
	 * Initialize the contents of the frame.
	 */
	@SuppressWarnings("serial")
	private void initialize() {
		//creating the main frame
		frmNetworkWatcher = new JFrame();
		frmNetworkWatcher.setTitle("Network Watcher");
		frmNetworkWatcher.setBounds(100, 100, 850, 350);
		frmNetworkWatcher.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		//creating the scroll pane for the table. 
		JScrollPane scrollPane = new JScrollPane();
		frmNetworkWatcher.getContentPane().add(scrollPane, BorderLayout.CENTER);
		//creating the table
		table = new JTable() {
			//default settings for table. 
			{
				//removes any margines between the rows and the columns
				setIntercellSpacing(new Dimension(0, 0));
			}
			
			@Override
			public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
				Component c = super.prepareRenderer(renderer, row, column);
				//alternate row highlighting
				if (!isRowSelected(row))
					c.setBackground(row % 2 == 0 ? getBackground() : new Color(0xE2F4FB));
				return c;
			}
			
		};
		scrollPane.setViewportView(table);
		table.setModel(model);
		table.setAutoCreateRowSorter(true);
		table.setDefaultRenderer(Date.class, new DefaultTableCellRenderer(){
			@Override
			public Component getTableCellRendererComponent(JTable table,Object value, boolean isSelected, boolean hasFocus,int row, int column) {
				super.getTableCellRendererComponent(table, value, isSelected, hasFocus,row, column);
				if (value instanceof Date) { //safety code and handles nulls.
					setText(FORMAT_DATE_TIME.format(value));
				}
				return this;
			}
			
		});
		//set the default table columns widths
		table.getColumnModel().getColumn(0).setPreferredWidth(100);
		table.getColumnModel().getColumn(1).setPreferredWidth(100);		
		table.getColumnModel().getColumn(2).setPreferredWidth(175);
		table.getColumnModel().getColumn(3).setPreferredWidth(175);
		table.getColumnModel().getColumn(4).setPreferredWidth(175);
		//start the scheduled task to scan the network. 
		timer.scheduleAtFixedRate(scanNetworkTask, 0, 60000); //start immediately, run every 60 secs. 
	}

	class ScanNetworkTask extends TimerTask {
		@Override
		public void run() {
			//this scans date and time. 
			final Date currentScan = new Date();
		    int[] bounds = ScanNet.rangeFromCidr(networkRange);

		    for (int i = bounds[0]; i <= bounds[1]; i++) {
		        String address = InetRange.intToIp(i);

		        try {
			        InetAddress ip = InetAddress.getByName(address);
			        
		            if (ip.isReachable(300)) { // Try for 300 millis
		            	String strMac = "";
		            	String strIP = ip.getHostAddress();
		            	Date lastScanned = new Date();
		            	//Run external application to get MAC address. 
		                System.out.printf("%s is reachable (%s)\n", ip.getHostAddress(), ip.getHostName());
		                Process proc = Runtime.getRuntime().exec("arp " + ip.getHostAddress());
		                BufferedReader rd = new BufferedReader( new InputStreamReader(proc.getInputStream() ) );
		                String arpOut = rd.readLine();
		                String[] arpArray = arpOut.split(" ");
		                strMac = arpArray[3];
		                System.out.println( " "+strMac);
		                //check for a valid mac address, just looking for a couple of semi colons for now. 
		                //process the new device and add/update the subfile.
		                if (strMac.split(":").length > 1) { 
		                	WatchedDevice dev = new WatchedDevice();
		                	int indexOfMac = model.indexOf(strMac);
		                	if (indexOfMac >= 0) { //use the existing device if already found;
		                		dev = model.getDevices().get(indexOfMac);
		                		System.out.println("updating -> mac:"+ strMac + " ip:"+strIP);
		                	} else { //new devices need an id (mac) and an insert date
		                		dev.setMacAddress(strMac);
		                		dev.setInserted(new Date());
			                	//add to the subfile (model)
		                		model.add(dev);
		                		System.out.println("inserting -> mac:"+ strMac + " ip:"+strIP);
		                	}
		                	//update the rest of the fields whether new or existing
		                	dev.setLastScanned(currentScan);
		                	dev.setLastReached(lastScanned); //update last date/time reached.
		                	dev.setIpAddress(strIP); //update IP;
		                	
		                	//notify of the chages to the model
		                	model.update(dev, false); //do not replace exiting dev object, just notify the model. 
		                	
		                } else {
		                	System.out.println("ignoring -> mac:"+ strMac + " ip:"+strIP);
		                }
		            }
		        } catch (Exception e) {
		            System.out.println("-Error accessing " + address + " -> "+e.getMessage());
//		            e.printStackTrace();
		        }
		    }
		}
		
	}
	
	@SuppressWarnings("serial")
	class ActiveDeviceModel extends AbstractTableModel {
		
		ArrayList<WatchedDevice> devices = new ArrayList<WatchedDevice>();
		
		Object[] colNames = new Object[]{
				"MAC", "IP", "Inserted", "Last Reached", "Last Scanned"
		};
		
		public synchronized ArrayList<WatchedDevice> getDevices() {
			return devices;
		}

		public synchronized void setDevices(ArrayList<WatchedDevice> devices) {
			this.devices = devices;
		}
		
		public int indexOf(String mac) {
			return getDevices().indexOf(new WatchedDevice(mac));
		}

		public void add(WatchedDevice dev) {
			int insertedAt = getDevices().size();
			getDevices().add(dev);
			fireTableRowsInserted(insertedAt, insertedAt);
		}
		
		public void update(WatchedDevice dev, boolean replaceExisting) {
			int indexOf = getDevices().indexOf(dev);
			if (indexOf > -1) {
				if (replaceExisting) {
					getDevices().set(indexOf, dev);
				}
				fireTableRowsUpdated(indexOf, indexOf);
			} else {
				add(dev);
			}	
		}
		
		public void delete(WatchedDevice dev) {
			int indexOf = getDevices().indexOf(dev);
			if (getDevices().remove(dev)) { //if successfully removed notify the model
				fireTableRowsDeleted(indexOf, indexOf);
			}
		}
		
		@Override
		public int getColumnCount() {
			return colNames.length;
		}

		@Override
		public int getRowCount() {
			return getDevices().size();
		}

		@Override
		public Object getValueAt(int row, int col) {
			WatchedDevice dev = getDevices().get(row);
			switch (col) {
			case 0:
				return dev.getMacAddress();
			case 1:
				return dev.getIpAddress();
			case 2:
				return dev.getInserted();
			case 3:
				return dev.getLastReached();
			case 4: 
				return dev.getLastScanned();
			default:
				return "";
			} 
		}

		@Override
		public Class<?> getColumnClass(int col) {
			switch (col) {
			case 0:
				return String.class;
			case 1:
				return String.class;
			case 2:
				return Date.class;
			case 3:
				return Date.class;
			case 4: 
				return Date.class;
			default:
				return Object.class;
			} 
		}

		@Override
		public String getColumnName(int column) {
			return colNames[column].toString();
		}
		
	}
}
